param dashboard

The InteractiveImage command provided with datashader makes it simple to make an interactive plot of a very large dataset, but very often one will want to add additional interactive controls to filter your data, select columns for plotting, etc., which is not supported by InteractiveImage. One way to do that is to use ParamNB to instantiate some parameters and then have it run the subsequent cell whenever one of those parameters changes (via paramnb.Widgets(...,next_n=1) ).

This notebook illustrates a cleaner way using a HoloViews stream to connect the widgets and the plot. Requires conda install -c ioam/label/dev holoviews paramnb and installing GeoViews (which is only important for the map tile support).

Setup

In [1]:
import holoviews as hv
import geoviews as gv
import param, paramnb
import pandas as pd

from colorcet import cm
from bokeh.models import WMTSTileSource
from holoviews.operation.datashader import datashade
from holoviews.streams import RangeXY

hv.notebook_extension('bokeh')

%time df = pd.read_csv('../data/nyc_taxi.csv', usecols = ['passenger_count', \
                       'pickup_x', 'pickup_y', 'dropoff_x', 'dropoff_y'])
df.tail()
CPU times: user 14.4 s, sys: 1.18 s, total: 15.6 s
Wall time: 15.7 s
Out[1]:
passenger_count pickup_x pickup_y dropoff_x dropoff_y
10679302 2 -8.232298e+06 4.980860e+06 -8.232492e+06 4.979234e+06
10679303 2 -8.235721e+06 4.972331e+06 -8.234857e+06 4.971131e+06
10679304 1 -8.235341e+06 4.975470e+06 -8.234203e+06 4.981092e+06
10679305 1 -8.237594e+06 4.973844e+06 -8.235618e+06 4.973722e+06
10679306 1 -8.233229e+06 4.977946e+06 -8.234152e+06 4.977120e+06

Interactive plot

In [2]:
tiles = gv.WMTS(WMTSTileSource(url='https://server.arcgisonline.com/ArcGIS/rest/services/'
                                   'World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg'))
tile_options = dict(width=800,height=475,xaxis=None,yaxis=None,bgcolor='black',show_grid=False)  

passenger_counts = sorted(df.passenger_count.unique().tolist())

class Options(hv.streams.Stream):
    alpha       = param.Magnitude(default=0.75, doc="Alpha value for the map opacity")
    colormap    = param.ObjectSelector(default=cm["fire"], objects=cm.values())
    plot        = param.ObjectSelector(default="pickup",   objects=["pickup","dropoff"])
    passengers  = param.ObjectSelector(default=1,          objects=passenger_counts)
    
    def make_view(self, x_range=None, y_range=None, **kwargs):
        map_tiles = tiles(style=dict(alpha=self.alpha), plot=tile_options) 

        df_filt = df[df.passenger_count==self.passengers]
        points = hv.Points(gv.Dataset(df_filt, kdims=[self.plot+'_x', self.plot+'_y'], vdims=[]))
        taxi_trips = datashade(points, width=800, height=475, x_sampling=1, y_sampling=1, 
                               cmap=self.colormap, element_type=gv.Image,
                               dynamic=False, x_range=x_range, y_range=y_range)
                
        return map_tiles * taxi_trips

selector = Options(name="")
paramnb.Widgets(selector, callback=selector.update)
hv.DynamicMap(selector.make_view, kdims=[], streams=[selector, RangeXY()])
Out[2]:

If you are viewing a static copy of this page through Anaconda Cloud, the interactive controls will not be usable, but you can download a copy of this notebook and run it through Jupyter notebook for the interactive version. You can also view the controls and the image as a deployable dashboard using Jupyter Dashboards , which can be installed separately using conda install -c conda-forge jupyter_dashboards . Jupyter Dashboards is a Jupyter extension that lets you choose which cells to publish to a dashboard layout, with the result like this screenshot that can be deployed as a standalone server.










How does it work?

In this example, we define a class that declares certain parameters whose value a user might wish to change, with defaults, ranges, and documentation if desired. We then add a method make_view that constructs a HoloViews object using the values of these parameters, and we instantiate our new class as an object:

In [3]:
selector2 = Options(name="")
selector2
Out[3]:
Options(alpha=0.75,colormap=<matplotlib.colors.LinearSegmentedColormap object at 0x128b17c50>,passengers=1,plot='pickup')

Here we could change the values of the parameters manually if we wanted:

In [4]:
selector2.alpha=0.2
selector2.plot='dropoff'
selector2
Out[4]:
Options(alpha=0.2,colormap=<matplotlib.colors.LinearSegmentedColormap object at 0x128b17c50>,passengers=1,plot='dropoff')

But we'd rather have some interactive controls, so let's make some ipywidgets for the parameters, using ParamNB:

In [5]:
paramnb.Widgets(selector2, callback=selector2.update)

Now we have widgets that change the corresponding parameters on selector2 . Plus, whenever those values change, Widgets will call selector2.update() . But at this point, that method call does nothing, because we haven't registered anything with selector2 to watch for such events.

So, now we need to make something that could register for such events and generate the corresponding plot. Just getting a plot is simple, using the method we defined already:

In [6]:
%%output size=40
selector2.make_view()
Out[6]:

This makes a HoloViews "view" object, which is then automatically turned into a Bokeh plot in a Jupyter notebook. But this plot won't dynamically update when the widgets are adjusted; it just respects whatever parameter values selector2 had when make_view() was invoked. To make the connection between the HoloViews object and the parameter widgets, we need one last line:

hv.DynamicMap(selector2.make_view, kdims=[], streams=[selector, RangeXY()])

Here, a DynamicMap is a HoloViews wrapper that accepts a callback ( selector2.make_view in this case) that it will call when a new HoloViews object is needed. Finally, the streams argument subscribes this DynamicMap to two streams, requesting that those streams trigger an update on the DynamicMap when the widget values change (via the selector stream) and when the Bokeh zoom range changes (via the RangeXY() stream). In either case, the DynamicMap will then execute the provided command to generate a new view, and ultimately the plot on your screen will update. Success!

[The kdims argument is unused here, because all of the dynamism here is provided by widgets and range changes, but in general we could also declare some "key dimensions" (e.g. "date"), and then there would be additional sliders created automatically to select values along those dimensions, which would then also trigger a call to selector2.make_view .]

While the approach outlined here is more complicated than having a single callback (which is of course also supported directly by paramnb), what it achieves is to be able to very flexibly create and subscribe to a wide variety of different event streams, each individually very simple and straightforward to create but which can be combined in arbitrary ways to create complex interactive behavior. See the new HoloViews Streams tutorials for more information, which explains how to watch for selection events, click events, keypress events, etc., all of which can be used to provide interactive behavior for Bokeh plots beyond what paramnb's widgets support.


Right click to download this notebook from GitHub.